In [1]:
import pandas as pd 
import numpy as np 
import matplotlib.pyplot as plt
from plotly.figure_factory import create_distplot
import seaborn as sns
from random import choices
from scipy.stats import ttest_ind, shapiro
%matplotlib inline
C:\Users\kpaqk\anaconda3\lib\site-packages\numpy\_distributor_init.py:30: UserWarning: loaded more than 1 DLL from .libs:
C:\Users\kpaqk\anaconda3\lib\site-packages\numpy\.libs\libopenblas.WCDJNK7YVMPZQ2ME2ZZHJJRJ3JIKNDB7.gfortran-win_amd64.dll
C:\Users\kpaqk\anaconda3\lib\site-packages\numpy\.libs\libopenblas.XWYDX2IKJW2NMTWSFYNGFUWKQU3LYTCZ.gfortran-win_amd64.dll
  warnings.warn("loaded more than 1 DLL from .libs:"

МТС Библиотека¶

МТС Библиотека - приложение для чтения электронных книг, прессы и прослушивания аудиокниг, доступно для абонентов всех мобильных операторов, продукт экосистемы МТС.

В представленном датасете собраны данные по пользователям и книгам, а также по их взаимодействиям (прочтение книги пользователем) из сервиса МТС Библиотека. Данные по чтению пользователями книг собраны за 2 два года, с 01-01-2018 по 31-12-2019 включительно, и разбавлены случайным шумом. ID пользователей и книг анонимизированы.

Статистика по датасету:

  • 150к пользователей
  • 60к книг
  • 1.5м взаимодействий (для 285к из них известны рейтинги)

Содержание¶

users.csv¶

В данном файле содержится информация о пользователях:

  • user_id - ID пользователя, int64
  • age - возрастная группа пользователя, строка вида "M_N"

    данный признак - результат работы модели

    • 18_24 - от 18 до 24 лет включительно
    • 25_34 - от 25 до 34 лет включительно
    • 35_44 - от 35 до 44 лет включительно
    • 45_54 - от 45 до 54 лет включительно
    • 55_64 - от 55 до 64 лет включительно
    • 65_inf - от 65 и старше
    • NaN - неизвестно
  • sex - пол пользователя, 1/0

    данный признак - результат работы модели

    • 1 - мужчина
    • 0 - женщина
    • NaN - неизвестно

items.csv¶

В данном файле содержится информация о книгах:

  • item_id - ID книги, int64
  • title - название книги, строка
  • genres - жанры, строка с разделителем ','
  • authors - авторы, строка с разделителем ','
  • year - год публикации, строка [,потому что есть значения, которые нельзя автоматически привести к числовому значению]

interactions.csv¶

В данном файле содержится информация о взаимодействиях пользователей и книг:

  • user_id - ID пользователя, int64
  • item_id - ID книги, int64
  • progress - прогресс по чтению в процентах, int8
  • rating - рейнтинг книги выставленный пользователем, от 1 до 5, много пропущенных значений
  • start_date - дата начала чтения книги пользователем

Preprocessing¶

In [2]:
df = pd.read_csv('interactions.csv')
df_users = pd.read_csv('users.csv')
df_items = pd.read_csv('items.csv')

Interactions¶

In [3]:
df.head()
Out[3]:
user_id item_id progress rating start_date
0 126706 14433 80 NaN 2018-01-01
1 127290 140952 58 NaN 2018-01-01
2 66991 198453 89 NaN 2018-01-01
3 46791 83486 23 5.0 2018-01-01
4 79313 188770 88 5.0 2018-01-01
In [4]:
df.shape
Out[4]:
(1533078, 5)
In [5]:
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1533078 entries, 0 to 1533077
Data columns (total 5 columns):
 #   Column      Non-Null Count    Dtype  
---  ------      --------------    -----  
 0   user_id     1533078 non-null  int64  
 1   item_id     1533078 non-null  int64  
 2   progress    1533078 non-null  int64  
 3   rating      285356 non-null   float64
 4   start_date  1533078 non-null  object 
dtypes: float64(1), int64(3), object(1)
memory usage: 58.5+ MB

Так как очень много пропущенных значений в rating, удалю NaNы, чтобы упростить дальнейшую проверку гипотез.

In [6]:
df.dropna(inplace=True)
df['start_date'] = pd.to_datetime(df['start_date'])
df.reset_index(drop=True, inplace=True)
df.head()
Out[6]:
user_id item_id progress rating start_date
0 46791 83486 23 5.0 2018-01-01
1 79313 188770 88 5.0 2018-01-01
2 42797 315927 69 5.0 2018-01-01
3 23439 9762 74 4.0 2018-01-01
4 115029 34757 1 4.0 2018-01-01
In [7]:
df.shape
Out[7]:
(285356, 5)
In [8]:
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 285356 entries, 0 to 285355
Data columns (total 5 columns):
 #   Column      Non-Null Count   Dtype         
---  ------      --------------   -----         
 0   user_id     285356 non-null  int64         
 1   item_id     285356 non-null  int64         
 2   progress    285356 non-null  int64         
 3   rating      285356 non-null  float64       
 4   start_date  285356 non-null  datetime64[ns]
dtypes: datetime64[ns](1), float64(1), int64(3)
memory usage: 10.9 MB
In [9]:
plt.title('Гистограмма частот для progress')
sns.histplot(df['progress'], bins=10)
plt.show()
In [10]:
num = df['progress'].count()
num_full = (df['progress'] == 100).sum()
print(f"Всего пользователи прочитали {num} книг")
print(f"Из них прочитали до конца {num_full} ~ {num_full/num*100:.2f}%")
print(f"Из них не дочитали {num - num_full} ~ {(num - num_full)/num*100:.2f}%")
Всего пользователи прочитали 285356 книг
Из них прочитали до конца 111544 ~ 39.09%
Из них не дочитали 173812 ~ 60.91%
In [11]:
plt.title('Гистограмма частот для rating')
sns.histplot(df['rating'], bins=10)
plt.show()
In [12]:
rating = df['rating'].count()
for i in range(5, 0, -1):
    print(f"{(df['rating']==i).sum()/rating*100:.2f}% пользователей оценили книгу {i}" )
75.24% пользователей оценили книгу 5
14.15% пользователей оценили книгу 4
6.40% пользователей оценили книгу 3
2.05% пользователей оценили книгу 2
2.14% пользователей оценили книгу 1
In [13]:
plt.title('Гистограмма частот для start_date')
sns.histplot(df['start_date'])
plt.show()
In [14]:
plt.title('Гистограмма частот для item_id')
sns.histplot(df['item_id'])
plt.show()
In [15]:
plt.title('Гистограмма частот для user_id')
sns.histplot(df['user_id'])
plt.show()

Выводы по interactions¶

  • Большинство пользователей не дочитали книгу до конца, но примерно треть дочитала.
  • 75% пользователей довольны прочитанной книгой

Users¶

In [16]:
df_users.head()
Out[16]:
user_id age sex
0 1 45_54 NaN
1 2 18_24 0.0
2 3 65_inf 0.0
3 4 18_24 0.0
4 5 35_44 0.0
In [17]:
df_users.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 142888 entries, 0 to 142887
Data columns (total 3 columns):
 #   Column   Non-Null Count   Dtype  
---  ------   --------------   -----  
 0   user_id  142888 non-null  int64  
 1   age      142742 non-null  object 
 2   sex      136626 non-null  float64
dtypes: float64(1), int64(1), object(1)
memory usage: 3.3+ MB
In [18]:
df_users.dropna(inplace=True)
In [19]:
plt.title('Гистограмма частот для age')
sns.histplot(df_users['age'])
plt.show()
In [20]:
age_all = df_users['age'].count()
for age in sorted(df_users['age'].unique()):
    print(f"Всего {(df_users['age']==age).sum()/age_all*100:.2f}%",
          f"пользователей в возрасте от {' до '.join(age.replace('inf', '100').split('_'))} лет")
Всего 39.16% пользователей в возрасте от 18 до 24 лет
Всего 20.85% пользователей в возрасте от 25 до 34 лет
Всего 12.12% пользователей в возрасте от 35 до 44 лет
Всего 9.94% пользователей в возрасте от 45 до 54 лет
Всего 11.57% пользователей в возрасте от 55 до 64 лет
Всего 6.35% пользователей в возрасте от 65 до 100 лет
In [21]:
plt.title('Гистограмма частот для пола')
sns.histplot(df_users['sex'], bins=10)
plt.show()
In [22]:
(df_users['sex'] == 0).sum()
Out[22]:
91897
In [23]:
print(f"{(df_users['sex'] == 0).sum()/df_users['sex'].count()*100:.2f}% пользователей женского пола")
print(f"{(df_users['sex'] == 1).sum()/df_users['sex'].count()*100:.2f}% пользователей мужского пола")
67.33% пользователей женского пола
32.67% пользователей мужского пола
In [24]:
for age in sorted(df_users['age'].unique()):
    df_age = df_users[df_users['age'] == age]
    print(f"■ Среди пользователей от {' до '.join(age.replace('inf', '100').split('_'))} лет ")
    print(f"{(df_age['sex'] == 0).sum()/df_age['sex'].count()*100:.2f}% пользователей женского пола")
    print(f"{(df_age['sex'] == 1).sum()/df_age['sex'].count()*100:.2f}% пользователей мужского пола\n")                      
■ Среди пользователей от 18 до 24 лет 
68.43% пользователей женского пола
31.57% пользователей мужского пола

■ Среди пользователей от 25 до 34 лет 
68.26% пользователей женского пола
31.74% пользователей мужского пола

■ Среди пользователей от 35 до 44 лет 
67.28% пользователей женского пола
32.72% пользователей мужского пола

■ Среди пользователей от 45 до 54 лет 
65.77% пользователей женского пола
34.23% пользователей мужского пола

■ Среди пользователей от 55 до 64 лет 
65.20% пользователей женского пола
34.80% пользователей мужского пола

■ Среди пользователей от 65 до 100 лет 
63.97% пользователей женского пола
36.03% пользователей мужского пола

Выводы по users¶

  • 66% пользователей женского пола, причем процентное соотношение мужчин и женщин не зависит от возраста группы
  • 60% пользователей имеют возраст от 18 до 34 лет

Items¶

In [25]:
df_items.head()
Out[25]:
id title genres authors year
0 128115 Ворон-челобитчик Зарубежные детские книги,Сказки,Зарубежная кла... Михаил Салтыков-Щедрин 1886
1 210979 Скрипка Ротшильда Классическая проза,Литература 19 века,Русская ... Антон Чехов 1894
2 95632 Испорченные дети Зарубежная классика,Классическая проза,Литерат... Михаил Салтыков-Щедрин 1869
3 247906 Странный человек Пьесы и драматургия,Литература 19 века Михаил Лермонтов 1831
4 294280 Господа ташкентцы Зарубежная классика,Классическая проза,Литерат... Михаил Салтыков-Щедрин 1873
In [26]:
df_items.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 59599 entries, 0 to 59598
Data columns (total 5 columns):
 #   Column   Non-Null Count  Dtype 
---  ------   --------------  ----- 
 0   id       59599 non-null  int64 
 1   title    59599 non-null  object
 2   genres   59568 non-null  object
 3   authors  52714 non-null  object
 4   year     46720 non-null  object
dtypes: int64(1), object(4)
memory usage: 2.3+ MB
In [27]:
df_items.nunique()
Out[27]:
id         59599
title      57358
genres     10769
authors    17265
year        1053
dtype: int64

Выводы по items¶

  • Просто датасет с информацией о книгах :(

Гипотезы¶

Поставлю гипотезу "Зависят ли оценка пользователя от пола?"

  1. Сформулирую нулевую и альтернативную гипотезу.

H0 - Различий между оценкими нет

H1 - Существуют резличия между оценками

Сформируем два образца для испытаний.

In [28]:
df_merge = pd.merge(df, df_users, how='inner')
men = df_merge[df_merge['sex'] == 1]
women = df_merge[df_merge['sex'] == 0]
In [29]:
print(f"Men mean: {men['rating'].mean():.2f}, women mean: {women['rating'].mean():.2f}")
Men mean: 4.57, women mean: 4.59
In [30]:
fig_1 = create_distplot([men['rating'], women['rating']], 
                         ['Men', 'Women'], 
                         colors=['gray', 'red',],
                         bin_size=.2, 
                         show_rug=False)

fig_1.update_layout(title_text='Сomparison of the distribution density in the group Men and the group Women', 
                     template='simple_white',
                     font=dict(family='Arial', size=12, color='black'))
fig_1.show()
In [31]:
men_bootstrap = np.array([np.mean(choices(men['rating'].to_numpy(), k=2000)) for i in range(2000)])
In [32]:
women_bootstrap = np.array([np.mean(choices(women['rating'].to_numpy(), k=2000)) for i in range(2000)])
In [33]:
plt.title('Гистограмма частот для мужчин после бутстрапа')
sns.histplot(men_bootstrap)
plt.show()
In [34]:
plt.title('Гистограмма частот для женщин после бутстрапа')
sns.histplot(women_bootstrap)
plt.show()

Проверю распределение на нормальность. Использую "Shapiro Wilk test of normality".¶

In [35]:
alpha = 0.05

test_shap = shapiro(men_bootstrap)
print("■ Test for men")
print(f"Statistic: {test_shap.statistic:.3f}")
print('P-Value:', f'{test_shap.pvalue:.5f}')

if alpha < test_shap.pvalue:
    print('Не можем отклонить H0 (данные распределены нормально)')
else:
    print('Отклоняем H0 (данные распределены нормально)')

print()
    
test_shap = shapiro(women_bootstrap)
print("■ Test for women")
print(f"Statistic: {test_shap.statistic:.3f}")
print('P-Value:', f'{test_shap.pvalue:.5f}')

if alpha < test_shap.pvalue:
    print('Не можем отклонить H0 (данные распределены нормально)')
else:
    print('Отклоняем H0 (данные распределены нормально)')
■ Test for men
Statistic: 0.999
P-Value: 0.25353
Не можем отклонить H0 (данные распределены нормально)

■ Test for women
Statistic: 0.999
P-Value: 0.41449
Не можем отклонить H0 (данные распределены нормально)

Теста Стьюдента¶

In [36]:
ttest = ttest_ind(men_bootstrap, women_bootstrap, equal_var=False)
print("■ Ttest for men and women")
print(f"Statistic: {ttest.statistic:.3f}")
print('P-Value:', f'{ttest.pvalue:.5f}')
if alpha < ttest.pvalue:
    print('Отклоняем H1')
else:
    print('Отклоняем H0')
■ Ttest for men and women
Statistic: -24.261
P-Value: 0.00000
Отклоняем H0

■ Отклоняем H0 - Различий между оценками нет. Значит разница есть. Следовательно, оценка книги зависит от пола читателя. Выбрал t-критерий Стьюдента, так как датасет достаточно большой, поэтому при бутстрапе данные распределены нормально, что необходимо для этого критерия.

Доверительный интервал¶

In [37]:
print("Доверительный интервал для мужчин")
(np.mean(men_bootstrap)-1.96*np.std(men_bootstrap)/np.sqrt(1000),
np.mean(men_bootstrap)+1.96*np.std(men_bootstrap)/np.sqrt(1000))
Доверительный интервал для мужчин
Out[37]:
(4.573505138851332, 4.575852361148669)
In [38]:
print("Доверительный интервал для женщин")
(np.mean(women_bootstrap)-1.96*np.std(women_bootstrap)/np.sqrt(1000), 
np.mean(women_bootstrap)+1.96*np.std(women_bootstrap)/np.sqrt(1000))
Доверительный интервал для женщин
Out[38]:
(4.588281176473816, 4.5907218235261835)

■ Построил доверительные интервалы, они не пересекаются. Следовательно, среднее значение рейтинга зависит от пола пользователя. Добавил этот пункт, чтобы убедиться в полученных результатах

In [39]:
fig_1 = create_distplot([men_bootstrap, women_bootstrap], 
                         ['Men', 'Women'], 
                         colors=['gray', 'red',],
                         bin_size=.005, 
                         show_rug=False)

fig_1.update_layout(title_text='Сomparison of the distribution density in the group Men and the group Women', 
                     template='simple_white',
                     font=dict(family='Arial', size=12, color='black'))
fig_1.show()

Выводы¶

  1. Был проведен EDA
  2. Построил статистические гипотезы
  3. Проверил гипотезы, используя критерий Стьюдента и доверительные интервалы, обосновал выбранные критерии